/*
 * Copyright (C) 2009 The Libphonenumber Authors
 * Copyright (C) 2017 Michael Rozumyanskiy
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.michaelrocks.libphonenumber.ohos;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import io.michaelrocks.libphonenumber.ohos.internal.RegexCache;

/**
 * A formatter which formats phone numbers as they are entered.
 *
 * <p>An AsYouTypeFormatter can be created by invoking
 * {@link PhoneNumberUtil#getAsYouTypeFormatter}. After that, digits can be added by invoking
 * {@link #inputDigit} on the formatter instance, and the partially formatted phone number will be
 * returned each time a digit is added. {@link #clear} can be invoked before formatting a new
 * number.
 *
 * <p>See the unittests for more details on how the formatter is to be used.
 *
 * @author Shaopeng Jia
 */
public class AsYouTypeFormatter {
    private String currentOutput = "";
    private StringBuilder formattingTemplate = new StringBuilder();
    private String currentFormattingPattern = "";
    private StringBuilder accruedInput = new StringBuilder();
    private StringBuilder accruedInputWithoutFormatting = new StringBuilder();
    private boolean ableToFormat = true;
    private boolean inputHasFormatting = false;

    private boolean isCompleteNumber = false;
    private boolean isExpectingCountryCallingCode = false;
    private final PhoneNumberUtil phoneUtil;
    private String defaultCountry;
    private static final char SEPARATOR_BEFORE_NATIONAL_NUMBER = ' ';
    private static final Phonemetadata.PhoneMetadata EMPTY_METADATA =
            new Phonemetadata.PhoneMetadata().setInternationalPrefix("NA");
    private Phonemetadata.PhoneMetadata defaultMetadata;
    private Phonemetadata.PhoneMetadata currentMetadata;

    private static final Pattern ELIGIBLE_FORMAT_PATTERN =
            Pattern.compile("[" + PhoneNumberUtil.VALID_PUNCTUATION + "]*"
                    + "(\\$\\d" + "[" + PhoneNumberUtil.VALID_PUNCTUATION + "]*)+");

    private static final Pattern NATIONAL_PREFIX_SEPARATORS_PATTERN = Pattern.compile("[- ]");
    private static final int MIN_LEADING_DIGITS_LENGTH = 3;
    private static final String DIGIT_PLACEHOLDER = "\u2008";
    private static final Pattern DIGIT_PATTERN = Pattern.compile(DIGIT_PLACEHOLDER);
    private int lastMatchPosition = 0;
    private int originalPosition = 0;
    private int positionToRemember = 0;
    private StringBuilder prefixBeforeNationalNumber = new StringBuilder();
    private boolean shouldAddSpaceAfterNationalPrefix = false;
    private String extractedNationalPrefix = "";
    private StringBuilder nationalNumber = new StringBuilder();
    private List<Phonemetadata.NumberFormat> possibleFormats = new ArrayList<Phonemetadata.NumberFormat>();
    private RegexCache regexCache = new RegexCache(64);

    /**
     * Constructs an as-you-type formatter. Should be obtained from {@link
     * PhoneNumberUtil#getAsYouTypeFormatter}.
     *
     * @param phoneUtil  an instance of {@link PhoneNumberUtil}
     * @param regionCode the country/region where the phone number is being entered
     */
    AsYouTypeFormatter(PhoneNumberUtil phoneUtil, String regionCode) {
        this.phoneUtil = phoneUtil;
        defaultCountry = regionCode;
        currentMetadata = getMetadataForRegion(defaultCountry);
        defaultMetadata = currentMetadata;
    }


    private Phonemetadata.PhoneMetadata getMetadataForRegion(String regionCode) {
        int countryCallingCode = phoneUtil.getCountryCodeForRegion(regionCode);
        String mainCountry = phoneUtil.getRegionCodeForCountryCode(countryCallingCode);
        Phonemetadata.PhoneMetadata metadata = phoneUtil.getMetadataForRegion(mainCountry);
        if (metadata != null) {
            return metadata;
        }
        return EMPTY_METADATA;
    }


    private boolean maybeCreateNewTemplate() {
        Iterator<Phonemetadata.NumberFormat> it = possibleFormats.iterator();
        while (it.hasNext()) {
            Phonemetadata.NumberFormat numberFormat = it.next();
            String pattern = numberFormat.getPattern();
            if (currentFormattingPattern.equals(pattern)) {
                return false;
            }
            if (createFormattingTemplate(numberFormat)) {
                currentFormattingPattern = pattern;
                shouldAddSpaceAfterNationalPrefix =
                        NATIONAL_PREFIX_SEPARATORS_PATTERN.matcher(
                                numberFormat.getNationalPrefixFormattingRule()).find();
                // With a new formatting template, the matched position using the old template needs to be
                // reset.
                lastMatchPosition = 0;
                return true;
            } else {
                it.remove();
            }
        }
        ableToFormat = false;
        return false;
    }

    private void getAvailableFormats(String leadingDigits) {

        boolean isInternationalNumber = isCompleteNumber && extractedNationalPrefix.length() == 0;
        List<Phonemetadata.NumberFormat> formatList =
                (isInternationalNumber && currentMetadata.intlNumberFormatSize() > 0)
                        ? currentMetadata.intlNumberFormats()
                        : currentMetadata.numberFormats();
        for (Phonemetadata.NumberFormat format : formatList) {

            if (extractedNationalPrefix.length() > 0
                    && PhoneNumberUtil.formattingRuleHasFirstGroupOnly(
                    format.getNationalPrefixFormattingRule())
                    && !format.getNationalPrefixOptionalWhenFormatting()
                    && !format.hasDomesticCarrierCodeFormattingRule()) {
                continue;
            } else if (extractedNationalPrefix.length() == 0
                    && !isCompleteNumber
                    && !PhoneNumberUtil.formattingRuleHasFirstGroupOnly(
                    format.getNationalPrefixFormattingRule())
                    && !format.getNationalPrefixOptionalWhenFormatting()) {
                continue;
            }
            if (ELIGIBLE_FORMAT_PATTERN.matcher(format.getFormat()).matches()) {
                possibleFormats.add(format);
            }
        }
        narrowDownPossibleFormats(leadingDigits);
    }

    private void narrowDownPossibleFormats(String leadingDigits) {
        int indexOfLeadingDigitsPattern = leadingDigits.length() - MIN_LEADING_DIGITS_LENGTH;
        Iterator<Phonemetadata.NumberFormat> it = possibleFormats.iterator();
        while (it.hasNext()) {
            Phonemetadata.NumberFormat format = it.next();
            if (format.leadingDigitsPatternSize() == 0) {

                continue;
            }
            int lastLeadingDigitsPattern =
                    Math.min(indexOfLeadingDigitsPattern, format.leadingDigitsPatternSize() - 1);
            Pattern leadingDigitsPattern = regexCache.getPatternForRegex(
                    format.getLeadingDigitsPattern(lastLeadingDigitsPattern));
            Matcher m = leadingDigitsPattern.matcher(leadingDigits);
            if (!m.lookingAt()) {
                it.remove();
            }
        }
    }

    private boolean createFormattingTemplate(Phonemetadata.NumberFormat format) {
        String numberPattern = format.getPattern();
        formattingTemplate.setLength(0);
        String tempTemplate = getFormattingTemplate(numberPattern, format.getFormat());
        if (tempTemplate.length() > 0) {
            formattingTemplate.append(tempTemplate);
            return true;
        }
        return false;
    }

    /**
     * getFormattingTemplate
     *
     * @param numberPattern numberPattern
     * @param numberFormat  numberFormat
     * @return template
     */
    private String getFormattingTemplate(String numberPattern, String numberFormat) {
        String longestPhoneNumber = "999999999999999";
        Matcher m = regexCache.getPatternForRegex(numberPattern).matcher(longestPhoneNumber);
        m.find();  // this will always succeed
        String aPhoneNumber = m.group();
        if (aPhoneNumber.length() < nationalNumber.length()) {
            return "";
        }
        String template = aPhoneNumber.replaceAll(numberPattern, numberFormat);
        template = template.replaceAll("9", DIGIT_PLACEHOLDER);
        return template;
    }

    /**
     * Clears the internal state of the formatter, so it can be reused.
     */
    public void clear() {
        currentOutput = "";
        accruedInput.setLength(0);
        accruedInputWithoutFormatting.setLength(0);
        formattingTemplate.setLength(0);
        lastMatchPosition = 0;
        currentFormattingPattern = "";
        prefixBeforeNationalNumber.setLength(0);
        extractedNationalPrefix = "";
        nationalNumber.setLength(0);
        ableToFormat = true;
        inputHasFormatting = false;
        positionToRemember = 0;
        originalPosition = 0;
        isCompleteNumber = false;
        isExpectingCountryCallingCode = false;
        possibleFormats.clear();
        shouldAddSpaceAfterNationalPrefix = false;
        if (!currentMetadata.equals(defaultMetadata)) {
            currentMetadata = getMetadataForRegion(defaultCountry);
        }
    }

    /**
     * Formats a phone number on-the-fly as each digit is entered.
     *
     * @param nextChar the most recently entered digit of a phone number. Formatting characters are
     *                 allowed, but as soon as they are encountered this method formats the number as entered and
     *                 not "as you type" anymore. Full width digits and Arabic-indic digits are allowed, and will
     *                 be shown as they are.
     * @return currentOutput the partially formatted phone number.
     */
    public String inputDigit(char nextChar) {
        currentOutput = inputDigitWithOptionToRememberPosition(nextChar, false);
        return currentOutput;
    }

    /**
     * Same as {@link #inputDigit}, but remembers the position where {@code nextChar} is inserted, so
     * that it can be retrieved later by using {@link #getRememberedPosition}. The remembered
     * position will be automatically adjusted if additional formatting characters are later
     * inserted/removed in front of {@code nextChar}.
     *
     * @param nextChar the most recentl
     * @return currentOutput
     */
    public String inputDigitAndRememberPosition(char nextChar) {
        currentOutput = inputDigitWithOptionToRememberPosition(nextChar, true);
        return currentOutput;
    }

    @SuppressWarnings("fallthrough")
    private String inputDigitWithOptionToRememberPosition(char nextChar, boolean rememberPosition) {
        accruedInput.append(nextChar);
        if (rememberPosition) {
            originalPosition = accruedInput.length();
        }
        if (!isDigitOrLeadingPlusSign(nextChar)) {
            ableToFormat = false;
            inputHasFormatting = true;
        } else {
            nextChar = normalizeAndAccrueDigitsAndPlusSign(nextChar, rememberPosition);
        }
        if (!ableToFormat) {
            if (inputHasFormatting) {
                return accruedInput.toString();
            } else if (attemptToExtractIdd()) {
                if (attemptToExtractCountryCallingCode()) {
                    return attemptToChoosePatternWithPrefixExtracted();
                }
            } else if (ableToExtractLongerNdd()) {
                prefixBeforeNationalNumber.append(SEPARATOR_BEFORE_NATIONAL_NUMBER);
                return attemptToChoosePatternWithPrefixExtracted();
            }
            return accruedInput.toString();
        }

        switch (accruedInputWithoutFormatting.length()) {
            case 0:
            case 1:
            case 2:
                return accruedInput.toString();
            case 3:
                if (attemptToExtractIdd()) {
                    isExpectingCountryCallingCode = true;
                } else {
                    extractedNationalPrefix = removeNationalPrefixFromNationalNumber();
                    return attemptToChooseFormattingPattern();
                }
            default:
                if (isExpectingCountryCallingCode) {
                    if (attemptToExtractCountryCallingCode()) {
                        isExpectingCountryCallingCode = false;
                    }
                    return prefixBeforeNationalNumber + nationalNumber.toString();
                }
                if (possibleFormats.size() > 0) {  // The formatting patterns are already chosen.
                    String tempNationalNumber = inputDigitHelper(nextChar);
                    String formattedNumber = attemptToFormatAccruedDigits();
                    if (formattedNumber.length() > 0) {
                        return formattedNumber;
                    }
                    narrowDownPossibleFormats(nationalNumber.toString());
                    if (maybeCreateNewTemplate()) {
                        return inputAccruedNationalNumber();
                    }
                    return ableToFormat
                            ? appendNationalNumber(tempNationalNumber)
                            : accruedInput.toString();
                } else {
                    return attemptToChooseFormattingPattern();
                }
        }
    }

    private String attemptToChoosePatternWithPrefixExtracted() {
        ableToFormat = true;
        isExpectingCountryCallingCode = false;
        possibleFormats.clear();
        lastMatchPosition = 0;
        formattingTemplate.setLength(0);
        currentFormattingPattern = "";
        return attemptToChooseFormattingPattern();
    }


    String getExtractedNationalPrefix() {
        return extractedNationalPrefix;
    }

    private boolean ableToExtractLongerNdd() {
        if (extractedNationalPrefix.length() > 0) {

            nationalNumber.insert(0, extractedNationalPrefix);
            int indexOfPreviousNdd = prefixBeforeNationalNumber.lastIndexOf(extractedNationalPrefix);
            prefixBeforeNationalNumber.setLength(indexOfPreviousNdd);
        }
        return !extractedNationalPrefix.equals(removeNationalPrefixFromNationalNumber());
    }

    private boolean isDigitOrLeadingPlusSign(char nextChar) {
        return Character.isDigit(nextChar)
                || (accruedInput.length() == 1
                && PhoneNumberUtil.PLUS_CHARS_PATTERN.matcher(Character.toString(nextChar)).matches());
    }

    String attemptToFormatAccruedDigits() {
        for (Phonemetadata.NumberFormat numberFormat : possibleFormats) {
            Matcher m = regexCache.getPatternForRegex(numberFormat.getPattern()).matcher(nationalNumber);
            if (m.matches()) {
                shouldAddSpaceAfterNationalPrefix =
                        NATIONAL_PREFIX_SEPARATORS_PATTERN.matcher(
                                numberFormat.getNationalPrefixFormattingRule()).find();
                String formattedNumber = m.replaceAll(numberFormat.getFormat());

                String fullOutput = appendNationalNumber(formattedNumber);
                String formattedNumberDigitsOnly = PhoneNumberUtil.normalizeDiallableCharsOnly(fullOutput);
                if (formattedNumberDigitsOnly.contentEquals(accruedInputWithoutFormatting)) {
                    return fullOutput;
                }
            }
        }
        return "";
    }

    /**
     * Returns the current position in the partially formatted phone number of the character which was
     *
     * @return currentOutputIndex
     */
    public int getRememberedPosition() {
        if (!ableToFormat) {
            return originalPosition;
        }
        int accruedInputIndex = 0;
        int currentOutputIndex = 0;
        while (accruedInputIndex < positionToRemember && currentOutputIndex < currentOutput.length()) {
            if (accruedInputWithoutFormatting.charAt(accruedInputIndex)
                    == currentOutput.charAt(currentOutputIndex)) {
                accruedInputIndex++;
            }
            currentOutputIndex++;
        }
        return currentOutputIndex;
    }

    /**
     * Combines the national number with any prefix (IDD/+ and country code or national prefix) that
     *
     * @param nationalNumber nationalNumber
     * @return return prefixBeforeNationalNumber + nationalNumber;
     */
    private String appendNationalNumber(String nationalNumber) {
        int prefixBeforeNationalNumberLength = prefixBeforeNationalNumber.length();
        if (shouldAddSpaceAfterNationalPrefix && prefixBeforeNationalNumberLength > 0
                && prefixBeforeNationalNumber.charAt(prefixBeforeNationalNumberLength - 1)
                != SEPARATOR_BEFORE_NATIONAL_NUMBER) {
            // We want to add a space after the national prefix if the national prefix formatting rule
            // indicates that this would normally be done, with the exception of the case where we already
            // appended a space because the NDD was surprisingly long.
            return new String(prefixBeforeNationalNumber) + SEPARATOR_BEFORE_NATIONAL_NUMBER
                    + nationalNumber;
        } else {
            return prefixBeforeNationalNumber + nationalNumber;
        }
    }


    /**
     * Attempts to set the formatting template
     *
     * @return formattedNumber
     */
    private String attemptToChooseFormattingPattern() {
        // We start to attempt to format only when at least MIN_LEADING_DIGITS_LENGTH digits of national
        // number (excluding national prefix) have been entered.
        if (nationalNumber.length() >= MIN_LEADING_DIGITS_LENGTH) {

            getAvailableFormats(nationalNumber.toString());
            // See if the accrued digits can be formatted properly already.
            String formattedNumber = attemptToFormatAccruedDigits();
            if (formattedNumber.length() > 0) {
                return formattedNumber;
            }
            return maybeCreateNewTemplate() ? inputAccruedNationalNumber() : accruedInput.toString();
        } else {
            return appendNationalNumber(nationalNumber.toString());
        }
    }


    /**
     * nvokes inputDigitHelper on each digit of the national number accrued, and returns a
     *
     * @return prefixBeforeNationalNumber
     */
    private String inputAccruedNationalNumber() {
        int lengthOfNationalNumber = nationalNumber.length();
        if (lengthOfNationalNumber > 0) {
            String tempNationalNumber = "";
            for (int i = 0; i < lengthOfNationalNumber; i++) {
                tempNationalNumber = inputDigitHelper(nationalNumber.charAt(i));
            }
            return ableToFormat ? appendNationalNumber(tempNationalNumber) : accruedInput.toString();
        } else {
            return prefixBeforeNationalNumber.toString();
        }
    }

    /**
     * Returns true if the current country
     *
     * @return nationalNumber
     */
    private boolean isNanpaNumberWithNationalPrefix() {
        return (currentMetadata.getCountryCode() == 1) && (nationalNumber.charAt(0) == '1')
                && (nationalNumber.charAt(1) != '0') && (nationalNumber.charAt(1) != '1');
    }

    private String removeNationalPrefixFromNationalNumber() {
        int startOfNationalNumber = 0;
        if (isNanpaNumberWithNationalPrefix()) {
            startOfNationalNumber = 1;
            prefixBeforeNationalNumber.append('1').append(SEPARATOR_BEFORE_NATIONAL_NUMBER);
            isCompleteNumber = true;
        } else if (currentMetadata.hasNationalPrefixForParsing()) {
            Pattern nationalPrefixForParsing =
                    regexCache.getPatternForRegex(currentMetadata.getNationalPrefixForParsing());
            Matcher m = nationalPrefixForParsing.matcher(nationalNumber);

            if (m.lookingAt() && m.end() > 0) {

                isCompleteNumber = true;
                startOfNationalNumber = m.end();
                prefixBeforeNationalNumber.append(nationalNumber.substring(0, startOfNationalNumber));
            }
        }
        String nationalPrefix = nationalNumber.substring(0, startOfNationalNumber);
        nationalNumber.delete(0, startOfNationalNumber);
        return nationalPrefix;
    }

    /**
     * Extracts IDD and plus sign to prefixBeforeNationalNumber when they are available, and places
     * the remaining input into nationalNumber.
     *
     * @return true when accruedInputWithoutFormatting begins with the plus sign or valid IDD for
     * defaultCountry.
     */
    private boolean attemptToExtractIdd() {
        Pattern internationalPrefix =
                regexCache.getPatternForRegex("\\" + PhoneNumberUtil.PLUS_SIGN + "|"
                        + currentMetadata.getInternationalPrefix());
        Matcher iddMatcher = internationalPrefix.matcher(accruedInputWithoutFormatting);
        if (iddMatcher.lookingAt()) {
            isCompleteNumber = true;
            int startOfCountryCallingCode = iddMatcher.end();
            nationalNumber.setLength(0);
            nationalNumber.append(accruedInputWithoutFormatting.substring(startOfCountryCallingCode));
            prefixBeforeNationalNumber.setLength(0);
            prefixBeforeNationalNumber.append(
                    accruedInputWithoutFormatting.substring(0, startOfCountryCallingCode));
            if (accruedInputWithoutFormatting.charAt(0) != PhoneNumberUtil.PLUS_SIGN) {
                prefixBeforeNationalNumber.append(SEPARATOR_BEFORE_NATIONAL_NUMBER);
            }
            return true;
        }
        return false;
    }

    /**
     * Extracts the country calling code from the beginning of nationalNumber to
     * prefixBeforeNationalNumber when they are available, and places the remaining input into
     * nationalNumber.
     *
     * @return true when
     */
    private boolean attemptToExtractCountryCallingCode() {
        if (nationalNumber.length() == 0) {
            return false;
        }
        StringBuilder numberWithoutCountryCallingCode = new StringBuilder();
        int countryCode = phoneUtil.extractCountryCode(nationalNumber, numberWithoutCountryCallingCode);
        if (countryCode == 0) {
            return false;
        }
        nationalNumber.setLength(0);
        nationalNumber.append(numberWithoutCountryCallingCode);
        String newRegionCode = phoneUtil.getRegionCodeForCountryCode(countryCode);
        if (PhoneNumberUtil.REGION_CODE_FOR_NON_GEO_ENTITY.equals(newRegionCode)) {
            currentMetadata = phoneUtil.getMetadataForNonGeographicalRegion(countryCode);
        } else if (!newRegionCode.equals(defaultCountry)) {
            currentMetadata = getMetadataForRegion(newRegionCode);
        }
        String countryCodeString = Integer.toString(countryCode);
        prefixBeforeNationalNumber.append(countryCodeString).append(SEPARATOR_BEFORE_NATIONAL_NUMBER);
        extractedNationalPrefix = "";
        return true;
    }

    private char normalizeAndAccrueDigitsAndPlusSign(char nextChar, boolean rememberPosition) {
        char normalizedChar;
        if (nextChar == PhoneNumberUtil.PLUS_SIGN) {
            normalizedChar = nextChar;
            accruedInputWithoutFormatting.append(nextChar);
        } else {
            int radix = 10;
            normalizedChar = Character.forDigit(Character.digit(nextChar, radix), radix);
            accruedInputWithoutFormatting.append(normalizedChar);
            nationalNumber.append(normalizedChar);
        }
        if (rememberPosition) {
            positionToRemember = accruedInputWithoutFormatting.length();
        }
        return normalizedChar;
    }

    private String inputDigitHelper(char nextChar) {
        Matcher digitMatcher = DIGIT_PATTERN.matcher(formattingTemplate);
        if (digitMatcher.find(lastMatchPosition)) {
            String tempTemplate = digitMatcher.replaceFirst(Character.toString(nextChar));
            formattingTemplate.replace(0, tempTemplate.length(), tempTemplate);
            lastMatchPosition = digitMatcher.start();
            return formattingTemplate.substring(0, lastMatchPosition + 1);
        } else {
            if (possibleFormats.size() == 1) {

                ableToFormat = false;
            }
            currentFormattingPattern = "";
            return accruedInput.toString();
        }
    }
}
